You have made your domain model to run a web application. This application is already a useful one, but still there are a lot of refinements that can be made to it. Let's do what's necessary to convert your application into a better application and, in this way you shall learn some new interesting things about OpenXava.
We'll start adding business logic to your entities in order to convert your application into something more than a simple database manager.
Calculated properties
Perhaps the most simple business logic you can add to your application is a calculated property. The properties you have used until now are persistent, i.e., each property is stored in a column in a table in the database. A calculated property is a property that does not store its value in the database but it's calculated any time the property is accessed. See the difference between a persistent and a calculated property.
// Persistent propertyprivateint quantity;// Has a field, so it's persistentpublicint getQuantity(){// A getter to return the field valuereturn quantity;}publicvoid setQuantity(int quantity){// Changes the field valuethis.quantity = quantity;}// Calculated propertypublicint getAmount(){// It has no field and no setter, only a getterreturn quantity * price;// with a calculation}
Calculated properties are automatically recognized by OpenXava. You can use them in views, tabular lists or any other part of your code.
We are going to use calculated properties to add the money element to our Invoicing applications. Because, we have details, products, quantities. But what about amounts?
Simple calculated property
The first step will be to add an amount property to the Detail. We want the detail amount to be recalculated and shown to the user when the user chooses a product and type in the quantity:
Adding this feature to your current code is practically adding a calculated property to Detail. Just add the next code to the Detail:
@Stereotype("MONEY")
@Depends("product.number, quantity")// When the user changes product or quantitypublicBigDecimal getAmount(){returnnewBigDecimal(quantity).multiply(product.getPrice());// this property is recalculated and redisplayed}
Simply put the calculation in getAmount() and use @Depends to indicate to OpenXava that the amount property depends on product.number and quantity, thus each time the user changes any of these values the property will be recalculated.
Now you have to add this new property to the details collection of CommercialDocument:
Nothing else is required. The mere addition of the getter and modifying the list properties is enough. Try the Invoice and Order modules to see the amount property in action.
Note: Verify that the products have their price recorded.
Using @DefaultValueCalculator
The way we calculated the amount for the detail line is not the best approach. There are at least two drawbacks to it. Firstly, the user may want to have the option to overwrite the unit price. Secondly, if the price of the product changes the amounts for all your invoices changes too, this is not good.
To avoid these drawbacks it's better to store the price of the product for each detail. Let's add a pricePerUnit persistent property to the Detail class and let's calculate its value from the price in Product using a @DefaultValueCalculator. Just to obtain the effect you can see:
The first obvious step is to add the property pricePerUnit. Add the next code to your Detail class:
@DefaultValueCalculator(
value=PricePerUnitCalculator.class, // This class calculates the initial value
properties=@PropertyValue(
name="productNumber", // The productNumber property of the calculator...
from="product.number")// ...is filled from product.number of the detail)
@Stereotype("MONEY")privateBigDecimal pricePerUnit;// A regular persistent property...publicBigDecimal getPricePerUnit(){// ...with its getter and setterreturn pricePerUnit==null?BigDecimal.ZERO : pricePerUnit;// Thus never returns null}publicvoid setPricePerUnit(BigDecimal pricePerUnit){this.pricePerUnit = pricePerUnit;}
PricePerUnitCalculator contains the logic to calculate the initial value. It simply reads the price from the product. See the next code for this calculator:
packageorg.openxava.invoicing.calculators;// In 'calculators' package
importorg.openxava.calculators.*;importorg.openxava.invoicing.model.*;importstaticorg.openxava.jpa.XPersistence.*;// For using getManager()publicclass PricePerUnitCalculator implements ICalculator {privateint productNumber;// Contains the product number when calculate() is calledpublicObject calculate()throwsException{
Product product = getManager()// getManager() from XPersistence
.find(Product.class, productNumber);// Find the productreturn product.getPrice();// Returns its price}publicvoid setProductNumber(int productNumber){this.productNumber = productNumber;}publicint getProductNumber(){return productNumber;}}
In this way when the user chooses a product the price per unit field is filled with the price of that product but because it's a persistent property, the user can change it. And if in the future the price of the product changes this price per unit of the detail will not change.
This means that you have to adapt your amount calculated property:
@Stereotype("MONEY")
@Depends("pricePerUnit, quantity")// pricePerUnit instead of product.numberpublicBigDecimal getAmount(){returnnewBigDecimal(quantity).multiply(getPricePerUnit());// getPricePerUnit() instead of product.getPrice()}
The getAmount() method uses pricePerUnit as source instead of product.price.
Finally, we have to edit the CommercialDocument entity and modify the list of properties to show in the collection to show the new property:
Try the Order and Invoice modules and observe the new behavior when adding details.
Calculated properties depending on a collection
We want to add amounts to Order and Invoice too. To calculate vat, base amount and total amount are indispensable. To do so you only need to add a few calculated properties to CommercialDocument class. The next figure shows the user interface for these properties:
Let's start with baseAmount. The next code shows its implementation.
publicBigDecimal getBaseAmount(){BigDecimal result = newBigDecimal("0.00");for(Detail detail: getDetails()){// We iterate through all details
result = result.add(detail.getAmount());// Adding up the amount}return result;}
The implementation is simple, just adding up the amount from all the details.
The next property to add is vatPercentage to calculate the vat. See the next code:
@Digits(integer=2, fraction=0)// To indicate its size
@Required
privateBigDecimal vatPercentage;publicBigDecimal getVatPercentage(){return vatPercentage == null?BigDecimal.ZERO : vatPercentage;// Thus never returns null}publicvoid setVatPercentage(BigDecimal vatPercentage){this.vatPercentage = vatPercentage;}
You can see that vatPercentage is a conventional persistent property. In this case we use @Digits (an annotation from the Hibernate Validator framework) as an alternative to @Column to specify the size.
We continue adding the vat property. See the next code:
Again a simple calculation.
Now that you have written the amount properties of CommercialDocument, you must modify the view by default to show vatPercentage and the list of properties of the collection to show the total properties of the CommercialDocument (Invoice and Order). Let's see a first approximation:
Try the Invoice module and you will see the new calculated properties, but if you try Order module the properties will not be displayed and you will get an ugly exception in the log of Eclipse IDE. This is because the total properties for Order are not defined, we have only defined the Invoice. Let's see how to define the total properties for Invoice and Order:
First we delete the details from CommercialDocument and declare an abstract method that will allow us to obtain the details of the subclasses of CommercialDocument.
Now let's see Invoice and Order:
publicclass Invoice extends CommercialDocument {
@ElementCollection
@ListProperties("product.number, product.description, quantity, pricePerUnit, " +
"amount[invoice.baseAmount, invoice.vat, invoice.totalAmount] ")privateCollection<Detail> details;// Details collection added// Getter addedpublicCollection<Detail> getDetails(){// This method implement the abstract method of 'CommercialDocument'return details;}publicvoid setDetails(Collection<Detail> details){// Setter addedthis.details = details;}// The rest of the source code
...
}
publicclass Order extends CommercialDocument {
@ElementCollection
@ListProperties("product.number, product.description, quantity, pricePerUnit, " +
"amount[order.baseAmount, order.vat, order.totalAmount] "// Entity parent 'Order' => [order.baseAmount, ...])privateCollection<Detail> details;// Getter addedpublicCollection<Detail> getDetails(){// This method implement the abstract method of 'CommercialDocument'return details;}publicvoid setDetails(Collection<Detail> details){// Setter addedthis.details = details;}// The rest of the source code
...
}
The source code added to Invoice and Order will generate two detail tables INVOICE_DETAILS and ORDER_DETAILS respectively.
Note: Delete the COMMERCIALDOCUMENT_DETAILS table, you learned to do it in the previous lesson.
Now you can try your application. It would behave almost as in figure at the begin of this section. “Almost” because vatPercentage does not have a default value yet. We add it in the next section.
Default value from a properties file
It's useful for the user to have the default value populated for the vatPercentage. You can use calculator (with @DefaultValueCalculator) that returns a fixed value. In this case changing the default value means changing your source code. Otherwise you can read the default value from the database (using JPA from your calculator). In that case changing the default value means updating a database table.
Another option is to store this configuration value in a properties file, a plain file with key=value pairs. In this case changing the default value for vatPercentage is just a matter of editing a plain file with a text editor.
Let's implement the properties file option. Create a file named invoicing.properties in the Invoicing/properties folder with the next content:
defaultVatPercentage=18
Though you can use the java.util.Properties class from Java to read this file we prefer to create a custom class to read these properties. We are going to call this class InvoicingPreferences and we'll put it in a new package named org.openxava.invoicing.util. You have the code here:
packageorg.openxava.invoicing.util;// in 'util' package
importjava.io.*;importjava.math.*;importjava.util.*;importorg.apache.commons.logging.*;importorg.openxava.util.*;publicclass InvoicingPreferences {privatefinalstaticString FILE_PROPERTIES="invoicing.properties";privatestatic Log log = LogFactory.getLog(InvoicingPreferences.class);privatestaticProperties properties;// We store the properties hereprivatestaticProperties getProperties(){if(properties == null){// We use lazy initialization
PropertiesReader reader = // PropertiesReader is a utility class from OpenXavanew PropertiesReader( InvoicingPreferences.class, FILE_PROPERTIES);try{
properties = reader.get();}catch(IOException ex){
log.error( XavaResources.getString(// To read a i18n message"properties_file_error", FILE_PROPERTIES), ex);
properties = newProperties();}}return properties;}publicstaticBigDecimal getDefaultVatPercentage(){// The only public methodreturnnewBigDecimal(getProperties().getProperty("defaultVatPercentage"));}}
As you can see InvoicingPreferences is a class with one static method, getDefaultVatPercentage(). The advantage of this class approach over the properties files is that if you change the way the preferences are obtained, for example reading from a database or an LDAP directory, you only have to change this class in your entire application.
You can use this class from the default calculator for the vatPercentage property. See the calculator in the next code:
packageorg.openxava.invoicing.calculators;// In 'calculators' package
importorg.openxava.calculators.*;// To use ICalculatorimportorg.openxava.invoicing.util.*;// To use InvoicingPreferencespublicclass VatPercentageCalculator implements ICalculator {publicObject calculate()throwsException{return InvoicingPreferences.getDefaultVatPercentage();}}
As you see, it just returns the defaultVatPercentage from InvoicingPreferences. Now, you can use this calculator in the definition of vatPercentage property in CommercialDocument:
With this code when the user clicks to create a new invoice, the vatPercentage field will be filled with 18 or whatever other value you put in invoicing.properties.
JPA callback methods
Another useful way to add business logic to your model is using JPA callback methods. A callback method is a method in your entity that is called in some specific moment of its life cycle as a persistent object. That is, you can specify some logic to execute on save, read, remove or modification of the entity.
In this section we'll see some practical applications of JPA callback methods.
Multiuser safe default value calculation
Until now we were calculating the Invoice and Order number using @DefaultValueCalculator. This calculates the default value when the user clicks to create a new Invoice or Order. So, if several users click on the “new” button at the same time all of them get the same number. This is not multiuser safe. The way to generate a unique number is by generating it just on save.
We are going to implement it using a JPA callback method. JPA allows you to mark any method of your class to be executed in any part of its life cycle. We'll indicate the calculation of the number just before the saving of the CommercialDocument. Using this approach we'll improve the number calculation for having a different numeration for Order and Invoice.
Edit the CommercialDocument entity and add the calculateNumber() method:
@PrePersist // Executed just before saving the object for the first timepublicvoid calculateNumber()throwsException{Query query = XPersistence.getManager()
.createQuery("select max(i.number) from " +
getClass().getSimpleName() + // Thus it's valid for both Invoice and Order" i where i.year = :year");
query.setParameter("year", year);Integer lastNumber = (Integer) query.getSingleResult();this.number = lastNumber == null?1 : lastNumber + 1;}
The previous code is the same as that of the NextNumberForYearCalculator but using getClass().getSimpleName() instead of “CommercialDocument”. The getSimpleName() method returns the name of the class without the package, i.e., just the entity name. It will be “Order” for Order and “Invoice” for Invoice. Thus we can get a different numeration for Order and Invoice.
JPA specification states that you should not use JPA API inside a JPA callback method. So the above method is not legal from a strict JPA viewpoint. But, Hibernate (the JPA implementation OX uses by default) allows you to use it in @PrePersist. And since JPA is the easier way to do this calculation we use it in our practice.
Now you can delete the NextNumberForYearCalculator class from your project, and modify the number property of CommercialDocument to avoid using it:
@Column(length=6)// @DefaultValueCalculator(value=NextNumberForYearCalculator.class, // Remove this// properties=@PropertyValue(name="year")// )
@ReadOnly // The user cannot modify the valueprivateint number;
Note that in addition to removing @DefaultValueCalculator, we have added the @ReadOnly annotation. This means that the user cannot enter or modify the number. This is the right approach given that the number is generated on saving the object, so the user typed value would always be overridden.
Try now the Invoice or Order module and you will see that the number is empty and not editable, and when you add the first detail, that saves the container document, the document number is calculated and updated in the user interface.
Synchronizing persistent and calculated properties
The approach towards calculating the vat, base amount and total amount of a commercial document is natural and practical. We used calculated properties that calculate, using pure Java, the values each time they are called.
But, calculated properties have some drawbacks. For example, if you want to do batch processing or a report for all invoices with a total amount between some range, it cannot be done using calculated properties. If you have a huge database the process will be very slow, because you will have to instantiate all invoices for calculating its total amount. For these cases having a persistent property, a column in the database, for the invoice or order amount, can yield far better performance.
In our case we'll maintain the current calculated properties, but we are going to add a new one, called amount, that will contain the same value as totalAmount, but this amount will be persistent with a corresponding column in the database. The tricky issue here is to have the amount property synchronized. We will use the JPA callback method in CommercialDocument (and one more trick) in order to achieve this.
The first step is to add the amount property to CommercialDocument. See the next code:
When the user adds, modifies or removes a detail in the user interface, the vat, base amount and total amount values are recalculated with fresh data instantly, however, in order to persist the changes in details the user must Save the CommercialDocument. To synchronize amount with totalAmount the first time we register a commercial document, we already know that we must use @PrePersist, but it turns out that JPA does not allow to mark more than one method with the same annotation, therefore, we will reorder our code. Let's see:
//@PrePersist // '@PrePersist' annotation deletedpublicvoid calculateNumber()throwsException{Query query = XPersistence.getManager()
.createQuery("select max(i.number) from " +
getClass().getSimpleName() + // Thus it's valid for both Invoice and Order" i where i.year = :year");
query.setParameter("year", year);Integer lastNumber = (Integer) query.getSingleResult();this.number = lastNumber==null?1 : lastNumber + 1;}
@PrePersist // Executed just before saving the object for the first timeprivatevoid onPrePersist()throwsException{
calculateNumber();
recalculateAmount();}publicvoid recalculateAmount(){
setAmount(getTotalAmount());}
Basically, we call the recalculateAmount() method every time a CommercialDocument entity is registered in the database for the first time. But recalculateAmount() must also be executed in the details updates. A first approximation might be to mark recalculateAmount with @PreUpdate, but it would be executed only when changes were made to the properties of CommercialDocument, not changes in details. We will solve this by executing the recalculateAmount() method whenever the user clicks save on a CommercialDocument. Let's see the next code:
@Version
privateInteger version;// 'version' property added, without getter or setter
@PreUpdate // '@PreUdate' addedpublicvoid recalculateAmount(){
setAmount(getTotalAmount());}
The version property property ensures that @PreUpdate callback is executed whenever the user Save a CommercialDocument, as this property will always be updated.
You can try the Invoice or Order module with this code, and you will see how when a detail line is added, removed or modified, the column in the database for amount is correctly updated after saving, ready to be used in massive processing.
Note: Delete the COMMERCIALDOCUMENT table to be recreated including the "version" column.
Database logic (@Formula)
Ideally you write all your business logic in Java, inside your entities. Nevertheless, sometimes that is not the most convenient way. Imagine that you have a calculated property in CommercialDocument, let's say estimatedProfit:
If you need to process all those invoices with an estimatedProfit greater than 1000, you have to code something like the next code:
Query query = getManager().createQuery("from Invoice");// No condition in queryfor(Object o: query.getResultList()){// Iterates over all objects
Invoice i = (Invoice) o;if(i.getEstimatedProfit()// Queries every object
.compareTo(newBigDecimal("1000"))>0){
i.doSomething();}}
You cannot use a condition in the query to discriminate by estimatedProfit, because estimatedProfit is not in the database, it's only in the Java object, so you have to instantiate every object in order to ask by the estimated profit. In some cases this way is a good option, but if you have a really huge amount of invoices, and only a few of them have the estimatedProfit greater than 1000, then your process will be very inefficient. What alternative do we have?
Our alternative is to use the @Formula annotation. @Formula is a Hibernate extension to the JPA standard, that allows you to map a property to a SQL statement. You can define estimatedProfit with @Formula as shown the next code:
@org.hibernate.annotations.Formula("AMOUNT * 0.10")// The calculation using SQL
@Stereotype("MONEY")privateBigDecimal estimatedProfit;// A field, as in the persistent property casepublicBigDecimal getEstimatedProfit(){// Only the getter is neededreturn estimatedProfit;}
This means that when a CommercialDocument is read from the database, the estimatedProfit field will be filled with the calculation for @Formula that is done by the database. The most useful thing of @Formula properties is that you can use it in different conditions, so that you can rewrite the above process as shown the next code:
Query query = getManager().createQuery("from Invoice i where i.estimatedProfit > :estimatedProfit");// Condition allowed
query.setParameter("estimatedProfit", newBigDecimal(1000));for(Object o: query.getResultList()){// Iterates only over selected objects
Invoice i = (Invoice) o;
i.doSomething();}
In this way we put the weight of calculating the estimated profit and selecting the record on the database server, and not on the Java server.
This fact also has effect in the list mode, because the user cannot filter or order by calculated properties, but he can do so using @Formula properties: @Formula is a good option for improving performance in some cases. Anyways, generally it's better to use calculated properties and writing your logic in Java. The advantage of calculated properties over @Formula is that your code is not database dependent. Moreover with calculated properties you can re-execute the calculation without reading the object from the database, so that you can use @Depends.
JUnit tests
Before we move on to the next lesson, we are going to write the JUnit code for this one. Remember, the code is not done if it has no tests. You can write the tests before, during or after coding. But you always have to write the tests.
The test code here is not only to give you a good example, but also to teach you ways to test different cases in your OpenXava application.
Modifying existing tests
Creating a new test for each new case seems like a good idea from a structural viewpoint, but in most cases it is not practical because in doing so your test code would grow very fast, and execution of all the tests for your application would take a substantial amount of time.
The more pragmatic approach is to modify the existing test code to cover all the new cases we have developed. Let's do it in this way.
In our case, all the code for this lesson applies to CommercialDocument, so we are going to modify the testCreate() method of CommercialDocumentTest to match the new functionality. We leave the testCreate() method as you see in the next code:
publicvoid testCreate()throwsException{
login("admin", "admin");
calculateNumber();// Added to calculate the next document number first
verifyDefaultValues();
chooseCustomer();
addDetails();
setOtherProperties();
save();
verifyAmountAndEstimatedProfit();// To test callback method and @Formula
verifyCreated();
remove();}
As you see, we add a new line in the beginning to calculate the next document number, and call the new verifyAmountAndEstimatedProfit() method.
Now it's more convenient to calculate the next document number in the beginning to use it during the test. To do this, change the old getNumber() method for the following two methods:
privatevoid calculateNumber(){Query query = getManager().createQuery("select max(i.number) from " +
model + // We change CommercialDocument for a variable" i where i.year = :year");
query.setParameter("year", Dates.getYear(newDate()));Integer lastNumber = (Integer) query.getSingleResult();if(lastNumber == null) lastNumber = 0;
number = Integer.toString(lastNumber + 1);}privateString getNumber(){return number;}
Previously we only had getNumber() to calculate and return the number, now we have a method to calculate (calculateNumber()), and another one to return the result (getNumber()). You can note that the calculation logic has a little change, instead of using “CommercialDocument” as the source of the query we use model, a variable. This is because now the numeration for invoices and orders are separated. We fill this variable, a field of the test class, in the test constructor, just as shows in the next code:
privateString model;// The model name to use in the query. Can be “Invoice” or “Order”public CommercialDocumentTest(String testName, String moduleName){super(testName, "Invoicing", moduleName);this.model = moduleName;// In this case module name matches model}
In this case module name, Invoice or Order, coincides with model name, Invoice or Order, so the easiest way to get the model name is from the module name.
Let's see the actual testing of the new functionality.
Testing default values and calculated properties
In this lesson we have done some modifications related to default values. Firstly, the default value for number is not calculated by means of @DefaultValueCalculator instead we use a JPA callback method. Secondly, we have a new property, vatPercentage, whose initial value is calculated by reading from a property file. To test these cases we have to modify the verifyDefaultValues() method as you see:
privatevoid verifyDefaultValues()throwsException{
execute("CRUD.new");
assertValue("year", getCurrentYear());// assertValue("number", getNumber()); // Now number has no initial value
assertValue("number", "");// on create a new document
assertValue("date", getCurrentDate());
assertValue("vatPercentage", "18");// Default value from properties file}
We test the vatPercentage default value calculation and we verify that the has no initial value, because now the number is not calculated until the document is saved (section Multiuser safe default value calculation). When the document (invoice or order) will be saved we'll verify that the number is calculated. When the detail is added we can test the amount for Detail calculation (section Simple calculated property), the default value calculation for pricePerUnit (section Using @DefaultValueCalculator) and the amount properties of the document (section Calculated properties depending on a collection). We'll test all this with a few modifications in the already existing addDetails() method:
privatevoid addDetails()throwsException{
assertCollectionRowCount("details", 0);// Adding a detail line
setValueInCollection("details", 0, "product.number", "1");
assertValueInCollection("details", 0,
"product.description", "Peopleware: Productive Projects and Teams");
assertValueInCollection("details", 0,
"pricePerUnit", "31.00");// @DefaultValueCalculator, section 'Using @DefaultValueCalculator'
setValueInCollection("details", 0, "quantity", "2");
assertValueInCollection("details", 0,
"amount", "62.00");// Calculated property, section 'Simple calculated property'// Verifying calculated properties of document
assertTotalInCollection("details", 0, "amount", "62.00");// Calculated properties
assertTotalInCollection("details", 1, "amount", "11.16");// depending on a collection,
assertTotalInCollection("details", 2,
"amount", "73.16");// section 'Calculated properties depending on a collection'// Adding another detail
setValueInCollection("details", 1, "product.number", "2");
assertValueInCollection("details", 1, "product.description", "Arco iris de lágrimas");
assertValueInCollection("details", 1,
"pricePerUnit", "15.00");// @DefaultValueCalculator, section 'Using @DefaultValueCalculator'
setValueInCollection("details", 1, "pricePerUnit", "10.00");// Modifying the default value
setValueInCollection("details", 1, "quantity", "1");
assertValueInCollection("details", 1, "amount", "10.00");
assertCollectionRowCount("details", 2);// Now we have 2 rows//Verifying calculated properties of document
assertTotalInCollection("details", 0, "amount", "72.00");
assertTotalInCollection("details", 1, "amount", "12.96");
assertTotalInCollection("details", 2, "amount", "84.96");}
As you see, with these simple modifications we test most of our new code. What remains is only the amount and estimatedProfit properties. We'll test them in the next section.
Testing calculated and persistent synchronized properties / @Formula
In section Synchronizing persistent and calculated properties we used a JPA callback method in CommercialDocument to have a persistent property, amount, synchronized with a calculated one, totalAmount. The amount property is only shown in list mode.
In section Database logic (@Formula) we have created a property that uses @Formula, estimatedProfit. This property is shown only in list mode.
Obviously, the simplest way to test it is by going to list mode and verifying that the values for these two properties are the expected ones. You have already seen that in testCreate() we call the verifyAmountAndEstimatedProfit(). Let's see its code:
privatevoid verifyAmountAndEstimatedProfit()throwsException{
execute("Mode.list");// Changes to list mode
setConditionValues(newString[]{// Filters to see in the list
getCurrentYear(), getNumber()// only the newly created document});
execute("List.filter");// Does the filter
assertValueInList(0, 0, getCurrentYear());// Verifies that
assertValueInList(0, 1, getNumber());// the filter has worked
assertValueInList(0, "amount", "84.96");// Asserts amount
assertValueInList(0, "estimatedProfit", "8.50");// Asserts estimatedProfit
execute("Mode.detailAndFirst");// Goes to detail mode}
Because we now go to list mode and then we go back to detail. We have to make a small modification to the verifyCreated() method, that is executed just after verifyAmountAndEstimatedProfit(). Let's see the modification in the next code:
privatevoid verifyCreated()throwsException{// setValue("year", getCurrentYear()); // We delete these lines// setValue("number", getNumber()); // for searching the document// execute("CRUD.search"); // because we already searched it with list mode// The rest of the test...
...
We remove these lines because now it's not necessary to search the newly created document. Now in the verifyAmountAndEstimatedProfit() method we went to list mode and chose the document, so we are already editing the document.
Congratulations! Now you have your tests up to date with your code. It's a good time to run all the tests for your application.
Summary
In this lesson you have learned some common ways to add business logic to your entities. There should be no doubt about the utility of calculated properties, callback methods or @Formula. Nevertheless, there are many other ways to add logic to your OpenXava application, and we are going to learn them.
In the coming lessons you'll see how to add validation, modify the standard module behavior and add your own business logic, among other ways to add custom logic to your application.
Table of Contents
Lesson 5: Basic business logic
You have made your domain model to run a web application. This application is already a useful one, but still there are a lot of refinements that can be made to it. Let's do what's necessary to convert your application into a better application and, in this way you shall learn some new interesting things about OpenXava.We'll start adding business logic to your entities in order to convert your application into something more than a simple database manager.
Calculated properties
Perhaps the most simple business logic you can add to your application is a calculated property. The properties you have used until now are persistent, i.e., each property is stored in a column in a table in the database. A calculated property is a property that does not store its value in the database but it's calculated any time the property is accessed. See the difference between a persistent and a calculated property.Calculated properties are automatically recognized by OpenXava. You can use them in views, tabular lists or any other part of your code.
We are going to use calculated properties to add the money element to our Invoicing applications. Because, we have details, products, quantities. But what about amounts?
Simple calculated property
The first step will be to add an amount property to the Detail. We want the detail amount to be recalculated and shown to the user when the user chooses a product and type in the quantity:Adding this feature to your current code is practically adding a calculated property to Detail. Just add the next code to the Detail:
Simply put the calculation in getAmount() and use @Depends to indicate to OpenXava that the amount property depends on product.number and quantity, thus each time the user changes any of these values the property will be recalculated.
Now you have to add this new property to the details collection of CommercialDocument:
Nothing else is required. The mere addition of the getter and modifying the list properties is enough. Try the Invoice and Order modules to see the amount property in action.
Verify that the products have their price recorded.
Using @DefaultValueCalculator
The way we calculated the amount for the detail line is not the best approach. There are at least two drawbacks to it. Firstly, the user may want to have the option to overwrite the unit price. Secondly, if the price of the product changes the amounts for all your invoices changes too, this is not good.To avoid these drawbacks it's better to store the price of the product for each detail. Let's add a pricePerUnit persistent property to the Detail class and let's calculate its value from the price in Product using a @DefaultValueCalculator. Just to obtain the effect you can see:
The first obvious step is to add the property pricePerUnit. Add the next code to your Detail class:
PricePerUnitCalculator contains the logic to calculate the initial value. It simply reads the price from the product. See the next code for this calculator:
In this way when the user chooses a product the price per unit field is filled with the price of that product but because it's a persistent property, the user can change it. And if in the future the price of the product changes this price per unit of the detail will not change.
This means that you have to adapt your amount calculated property:
The getAmount() method uses pricePerUnit as source instead of product.price.
Finally, we have to edit the CommercialDocument entity and modify the list of properties to show in the collection to show the new property:
Try the Order and Invoice modules and observe the new behavior when adding details.
Calculated properties depending on a collection
We want to add amounts to Order and Invoice too. To calculate vat, base amount and total amount are indispensable. To do so you only need to add a few calculated properties to CommercialDocument class. The next figure shows the user interface for these properties:Let's start with baseAmount. The next code shows its implementation.
The implementation is simple, just adding up the amount from all the details.
The next property to add is vatPercentage to calculate the vat. See the next code:
You can see that vatPercentage is a conventional persistent property. In this case we use @Digits (an annotation from the Hibernate Validator framework) as an alternative to @Column to specify the size.
We continue adding the vat property. See the next code:
It's a simple calculation.
Only totalAmount remains. You can see its code:
Again a simple calculation.
Now that you have written the amount properties of CommercialDocument, you must modify the view by default to show vatPercentage and the list of properties of the collection to show the total properties of the CommercialDocument (Invoice and Order). Let's see a first approximation:
Try the Invoice module and you will see the new calculated properties, but if you try Order module the properties will not be displayed and you will get an ugly exception in the log of Eclipse IDE. This is because the total properties for Order are not defined, we have only defined the Invoice. Let's see how to define the total properties for Invoice and Order:
First we delete the details from CommercialDocument and declare an abstract method that will allow us to obtain the details of the subclasses of CommercialDocument.
Now let's see Invoice and Order:
The source code added to Invoice and Order will generate two detail tables INVOICE_DETAILS and ORDER_DETAILS respectively.
Delete the COMMERCIALDOCUMENT_DETAILS table, you learned to do it in the previous lesson.
Default value from a properties file
It's useful for the user to have the default value populated for the vatPercentage. You can use calculator (with @DefaultValueCalculator) that returns a fixed value. In this case changing the default value means changing your source code. Otherwise you can read the default value from the database (using JPA from your calculator). In that case changing the default value means updating a database table.Another option is to store this configuration value in a properties file, a plain file with key=value pairs. In this case changing the default value for vatPercentage is just a matter of editing a plain file with a text editor.
Let's implement the properties file option. Create a file named invoicing.properties in the Invoicing/properties folder with the next content:
Though you can use the java.util.Properties class from Java to read this file we prefer to create a custom class to read these properties. We are going to call this class InvoicingPreferences and we'll put it in a new package named org.openxava.invoicing.util. You have the code here:
As you can see InvoicingPreferences is a class with one static method, getDefaultVatPercentage(). The advantage of this class approach over the properties files is that if you change the way the preferences are obtained, for example reading from a database or an LDAP directory, you only have to change this class in your entire application.
You can use this class from the default calculator for the vatPercentage property. See the calculator in the next code:
As you see, it just returns the defaultVatPercentage from InvoicingPreferences. Now, you can use this calculator in the definition of vatPercentage property in CommercialDocument:
With this code when the user clicks to create a new invoice, the vatPercentage field will be filled with 18 or whatever other value you put in invoicing.properties.
JPA callback methods
Another useful way to add business logic to your model is using JPA callback methods. A callback method is a method in your entity that is called in some specific moment of its life cycle as a persistent object. That is, you can specify some logic to execute on save, read, remove or modification of the entity.In this section we'll see some practical applications of JPA callback methods.
Multiuser safe default value calculation
Until now we were calculating the Invoice and Order number using @DefaultValueCalculator. This calculates the default value when the user clicks to create a new Invoice or Order. So, if several users click on the “new” button at the same time all of them get the same number. This is not multiuser safe. The way to generate a unique number is by generating it just on save.We are going to implement it using a JPA callback method. JPA allows you to mark any method of your class to be executed in any part of its life cycle. We'll indicate the calculation of the number just before the saving of the CommercialDocument. Using this approach we'll improve the number calculation for having a different numeration for Order and Invoice.
Edit the CommercialDocument entity and add the calculateNumber() method:
The previous code is the same as that of the NextNumberForYearCalculator but using getClass().getSimpleName() instead of “CommercialDocument”. The getSimpleName() method returns the name of the class without the package, i.e., just the entity name. It will be “Order” for Order and “Invoice” for Invoice. Thus we can get a different numeration for Order and Invoice.
JPA specification states that you should not use JPA API inside a JPA callback method. So the above method is not legal from a strict JPA viewpoint. But, Hibernate (the JPA implementation OX uses by default) allows you to use it in @PrePersist. And since JPA is the easier way to do this calculation we use it in our practice.
Now you can delete the NextNumberForYearCalculator class from your project, and modify the number property of CommercialDocument to avoid using it:
Note that in addition to removing @DefaultValueCalculator, we have added the @ReadOnly annotation. This means that the user cannot enter or modify the number. This is the right approach given that the number is generated on saving the object, so the user typed value would always be overridden.
Try now the Invoice or Order module and you will see that the number is empty and not editable, and when you add the first detail, that saves the container document, the document number is calculated and updated in the user interface.
Synchronizing persistent and calculated properties
The approach towards calculating the vat, base amount and total amount of a commercial document is natural and practical. We used calculated properties that calculate, using pure Java, the values each time they are called.But, calculated properties have some drawbacks. For example, if you want to do batch processing or a report for all invoices with a total amount between some range, it cannot be done using calculated properties. If you have a huge database the process will be very slow, because you will have to instantiate all invoices for calculating its total amount. For these cases having a persistent property, a column in the database, for the invoice or order amount, can yield far better performance.
In our case we'll maintain the current calculated properties, but we are going to add a new one, called amount, that will contain the same value as totalAmount, but this amount will be persistent with a corresponding column in the database. The tricky issue here is to have the amount property synchronized. We will use the JPA callback method in CommercialDocument (and one more trick) in order to achieve this.
The first step is to add the amount property to CommercialDocument. See the next code:
When the user adds, modifies or removes a detail in the user interface, the vat, base amount and total amount values are recalculated with fresh data instantly, however, in order to persist the changes in details the user must Save the CommercialDocument. To synchronize amount with totalAmount the first time we register a commercial document, we already know that we must use @PrePersist, but it turns out that JPA does not allow to mark more than one method with the same annotation, therefore, we will reorder our code. Let's see:
Basically, we call the recalculateAmount() method every time a CommercialDocument entity is registered in the database for the first time. But recalculateAmount() must also be executed in the details updates. A first approximation might be to mark recalculateAmount with @PreUpdate, but it would be executed only when changes were made to the properties of CommercialDocument, not changes in details. We will solve this by executing the recalculateAmount() method whenever the user clicks save on a CommercialDocument. Let's see the next code:
The version property property ensures that @PreUpdate callback is executed whenever the user Save a CommercialDocument, as this property will always be updated.
You can try the Invoice or Order module with this code, and you will see how when a detail line is added, removed or modified, the column in the database for amount is correctly updated after saving, ready to be used in massive processing.
Delete the COMMERCIALDOCUMENT table to be recreated including the "version" column.
Database logic (@Formula)
Ideally you write all your business logic in Java, inside your entities. Nevertheless, sometimes that is not the most convenient way. Imagine that you have a calculated property in CommercialDocument, let's say estimatedProfit:If you need to process all those invoices with an estimatedProfit greater than 1000, you have to code something like the next code:
You cannot use a condition in the query to discriminate by estimatedProfit, because estimatedProfit is not in the database, it's only in the Java object, so you have to instantiate every object in order to ask by the estimated profit. In some cases this way is a good option, but if you have a really huge amount of invoices, and only a few of them have the estimatedProfit greater than 1000, then your process will be very inefficient. What alternative do we have?
Our alternative is to use the @Formula annotation. @Formula is a Hibernate extension to the JPA standard, that allows you to map a property to a SQL statement. You can define estimatedProfit with @Formula as shown the next code:
This means that when a CommercialDocument is read from the database, the estimatedProfit field will be filled with the calculation for @Formula that is done by the database. The most useful thing of @Formula properties is that you can use it in different conditions, so that you can rewrite the above process as shown the next code:
In this way we put the weight of calculating the estimated profit and selecting the record on the database server, and not on the Java server.
This fact also has effect in the list mode, because the user cannot filter or order by calculated properties, but he can do so using @Formula properties:
@Formula is a good option for improving performance in some cases. Anyways, generally it's better to use calculated properties and writing your logic in Java. The advantage of calculated properties over @Formula is that your code is not database dependent. Moreover with calculated properties you can re-execute the calculation without reading the object from the database, so that you can use @Depends.
JUnit tests
Before we move on to the next lesson, we are going to write the JUnit code for this one. Remember, the code is not done if it has no tests. You can write the tests before, during or after coding. But you always have to write the tests.The test code here is not only to give you a good example, but also to teach you ways to test different cases in your OpenXava application.
Modifying existing tests
Creating a new test for each new case seems like a good idea from a structural viewpoint, but in most cases it is not practical because in doing so your test code would grow very fast, and execution of all the tests for your application would take a substantial amount of time.The more pragmatic approach is to modify the existing test code to cover all the new cases we have developed. Let's do it in this way.
In our case, all the code for this lesson applies to CommercialDocument, so we are going to modify the testCreate() method of CommercialDocumentTest to match the new functionality. We leave the testCreate() method as you see in the next code:
As you see, we add a new line in the beginning to calculate the next document number, and call the new verifyAmountAndEstimatedProfit() method.
Now it's more convenient to calculate the next document number in the beginning to use it during the test. To do this, change the old getNumber() method for the following two methods:
Previously we only had getNumber() to calculate and return the number, now we have a method to calculate (calculateNumber()), and another one to return the result (getNumber()). You can note that the calculation logic has a little change, instead of using “CommercialDocument” as the source of the query we use model, a variable. This is because now the numeration for invoices and orders are separated. We fill this variable, a field of the test class, in the test constructor, just as shows in the next code:
In this case module name, Invoice or Order, coincides with model name, Invoice or Order, so the easiest way to get the model name is from the module name.
Let's see the actual testing of the new functionality.
Testing default values and calculated properties
In this lesson we have done some modifications related to default values. Firstly, the default value for number is not calculated by means of @DefaultValueCalculator instead we use a JPA callback method. Secondly, we have a new property, vatPercentage, whose initial value is calculated by reading from a property file. To test these cases we have to modify the verifyDefaultValues() method as you see:We test the vatPercentage default value calculation and we verify that the has no initial value, because now the number is not calculated until the document is saved (section Multiuser safe default value calculation). When the document (invoice or order) will be saved we'll verify that the number is calculated. When the detail is added we can test the amount for Detail calculation (section Simple calculated property), the default value calculation for pricePerUnit (section Using @DefaultValueCalculator) and the amount properties of the document (section Calculated properties depending on a collection). We'll test all this with a few modifications in the already existing addDetails() method:
As you see, with these simple modifications we test most of our new code. What remains is only the amount and estimatedProfit properties. We'll test them in the next section.
Testing calculated and persistent synchronized properties / @Formula
In section Synchronizing persistent and calculated properties we used a JPA callback method in CommercialDocument to have a persistent property, amount, synchronized with a calculated one, totalAmount. The amount property is only shown in list mode.In section Database logic (@Formula) we have created a property that uses @Formula, estimatedProfit. This property is shown only in list mode.
Obviously, the simplest way to test it is by going to list mode and verifying that the values for these two properties are the expected ones. You have already seen that in testCreate() we call the verifyAmountAndEstimatedProfit(). Let's see its code:
Because we now go to list mode and then we go back to detail. We have to make a small modification to the verifyCreated() method, that is executed just after verifyAmountAndEstimatedProfit(). Let's see the modification in the next code:
We remove these lines because now it's not necessary to search the newly created document. Now in the verifyAmountAndEstimatedProfit() method we went to list mode and chose the document, so we are already editing the document.
Congratulations! Now you have your tests up to date with your code. It's a good time to run all the tests for your application.
Summary
In this lesson you have learned some common ways to add business logic to your entities. There should be no doubt about the utility of calculated properties, callback methods or @Formula. Nevertheless, there are many other ways to add logic to your OpenXava application, and we are going to learn them.In the coming lessons you'll see how to add validation, modify the standard module behavior and add your own business logic, among other ways to add custom logic to your application.
Download source code of this lesson
Any problem with this lesson? Ask in the forum Everything fine? Go to Lesson 6